damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

ActionViewController.swift (13949B)


      1 //
      2 //  ActionViewController.swift
      3 //  highlighter action extension
      4 //
      5 //  Created by Daniel D’Aquino on 2024-08-09.
      6 //
      7 
      8 import UIKit
      9 import MobileCoreServices
     10 import UniformTypeIdentifiers
     11 import SwiftUI
     12 
     13 struct ShareExtensionView: View {
     14     @State var highlighter_state: HighlighterState = .loading
     15     let extensionContext: NSExtensionContext
     16     @State var state: DamusState? = nil
     17     @State var signedEvent: String? = nil
     18     
     19     @State private var selectedText = ""
     20     @State private var selectedTextHeight: CGFloat = .zero
     21     @State private var selectedTextWidth: CGFloat = .zero
     22     
     23     @Environment(\.scenePhase) var scenePhase
     24     
     25     var body: some View {
     26         VStack(spacing: 15) {
     27             if let state {
     28                 switch self.highlighter_state {
     29                     case .loading:
     30                         ProgressView()
     31                     case .no_highlight_text:
     32                         Group {
     33                             Text("No text selected", comment: "Title indicating that a highlight cannot be posted because no text was selected.")
     34                                 .font(.largeTitle)
     35                                 .multilineTextAlignment(.center)
     36                                 .padding()
     37                             Text("You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.", comment: "Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue")
     38                                 .multilineTextAlignment(.center)
     39                             Button(action: {
     40                                 self.done()
     41                             }, label: {
     42                                 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
     43                             })
     44                             .foregroundStyle(.secondary)
     45                         }
     46                     case .not_logged_in:
     47                         Group {
     48                             Text("Not logged in", comment: "Title indicating that a highlight cannot be posted because the user is not logged in.")
     49                                 .font(.largeTitle)
     50                                 .multilineTextAlignment(.center)
     51                                 .padding()
     52                             Text("You cannot post a highlight because you are not logged in with a private key! Please close this, login with a private key (or nsec), and try again.", comment: "Label explaining a highlight cannot be made because the user is not logged in")
     53                                 .multilineTextAlignment(.center)
     54                             Button(action: {
     55                                 self.done()
     56                             }, label: {
     57                                 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
     58                             })
     59                             .foregroundStyle(.secondary)
     60                         }
     61                     case .loaded(let highlighted_text, let source_url):
     62                         PostView(
     63                             action: .highlighting(HighlightContentDraft(selected_text: highlighted_text, source: .external_url(source_url))),
     64                             damus_state: state
     65                         )
     66                     case .failed(let error):
     67                         Group {
     68                             Text("Error", comment: "Title indicating that an error has occurred.")
     69                                 .font(.largeTitle)
     70                                 .multilineTextAlignment(.center)
     71                                 .padding()
     72                             Text("An unexpected error occurred. Please contact Damus support via [Nostr](damus:npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955) or [email](support@damus.io) with the error message below.", comment: "Label explaining there was an error, and suggesting next steps")
     73                                 .multilineTextAlignment(.center)
     74                             Text("Error: \(error)")
     75                             Button(action: {
     76                                 self.done()
     77                             }, label: {
     78                                 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
     79                             })
     80                             .foregroundStyle(.secondary)
     81                         }
     82                     case .posted(event: let event):
     83                         Group {
     84                             Image(systemName: "checkmark.circle.fill")
     85                                 .resizable()
     86                                 .frame(width: 60, height: 60)
     87                             Text("Posted", comment: "Title indicating that the user has posted a highlight successfully")
     88                                 .font(.largeTitle)
     89                                 .multilineTextAlignment(.center)
     90                                 .padding(.bottom)
     91                             
     92                             Link(destination: URL(string: "damus:\(event.id.bech32)")!, label: {
     93                                 Text("Go to the app", comment: "Button label giving the user the option to go to the app after posting a highlight")
     94                             })
     95                             .buttonStyle(GradientButtonStyle())
     96                             Button(action: {
     97                                 self.done()
     98                             }, label: {
     99                                 Text("Close", comment: "Button label giving the user the option to close the sheet from which they posted a highlight")
    100                             })
    101                             .foregroundStyle(.secondary)
    102                         }
    103                     case .cancelled:
    104                         Group {
    105                             Text("Cancelled", comment: "Title indicating that the user has cancelled.")
    106                                 .font(.largeTitle)
    107                                 .padding()
    108                             Button(action: {
    109                                 self.done()
    110                             }, label: {
    111                                 Text("Close", comment: "Button label giving the user the option to close the sheet from which they were trying to post a highlight")
    112                             })
    113                             .foregroundStyle(.secondary)
    114                         }
    115                     case .posting:
    116                         Group {
    117                             ProgressView()
    118                                 .frame(width: 20, height: 20)
    119                             Text("Posting", comment: "Title indicating that the highlight post is being published to the network")
    120                                 .font(.largeTitle)
    121                                 .multilineTextAlignment(.center)
    122                                 .padding(.bottom)
    123                             Text("Your highlight is being broadcasted to the network. Please wait.", comment: "Label explaining there their highlight publishing action is in progress")
    124                                 .multilineTextAlignment(.center)
    125                                 .padding()
    126                         }
    127                 }
    128             }
    129         }
    130         .onAppear(perform: {
    131             self.loadSharedUrl()
    132             guard let keypair = get_saved_keypair() else { return }
    133             guard keypair.privkey != nil else {
    134                 self.highlighter_state = .not_logged_in
    135                 return
    136             }
    137             self.state = DamusState(keypair: keypair)
    138         })
    139         .onChange(of: self.highlighter_state) {
    140             if case .cancelled = highlighter_state {
    141                 self.done()
    142             }
    143         }
    144         .onReceive(handle_notify(.post)) { post_notification in
    145             switch post_notification {
    146                 case .post(let post):
    147                     self.post(post)
    148                 case .cancel:
    149                     self.highlighter_state = .cancelled
    150             }
    151         }
    152         .onChange(of: scenePhase) { (phase: ScenePhase) in
    153             guard let state else { return }
    154             switch phase {
    155             case .background:
    156                 print("txn: πŸ“™ HIGHLIGHTER BACKGROUNDED")
    157                 Task { @MainActor in
    158                     state.ndb.close()
    159                 }
    160                 break
    161             case .inactive:
    162                 print("txn: πŸ“™ HIGHLIGHTER INACTIVE")
    163                 break
    164             case .active:
    165                 print("txn: πŸ“™ HIGHLIGHTER ACTIVE")
    166                 state.pool.ping()
    167             @unknown default:
    168                 break
    169             }
    170         }
    171         .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
    172             guard let state else { return }
    173             print("txn: πŸ“™ HIGHLIGHTER ACTIVE NOTIFY")
    174             if state.ndb.reopen() {
    175                 print("txn: HIGHLIGHTER NOSTRDB REOPENED")
    176             } else {
    177                 print("txn: HIGHLIGHTER NOSTRDB FAILED TO REOPEN closed: \(state.ndb.is_closed)")
    178             }
    179         }
    180         .onReceive(NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)) { obj in
    181             guard let state else { return }
    182             print("txn: πŸ“™ HIGHLIGHTER BACKGROUNDED")
    183             Task { @MainActor in
    184                 state.ndb.close()
    185             }
    186         }
    187     }
    188     
    189     func loadSharedUrl() {
    190         guard
    191            let extensionItem = extensionContext.inputItems.first as? NSExtensionItem,
    192            let itemProvider = extensionItem.attachments?.first else {
    193             self.highlighter_state = .failed(error: "Can't get itemProvider")
    194             return
    195         }
    196         
    197         let propertyList = UTType.propertyList.identifier
    198         if itemProvider.hasItemConformingToTypeIdentifier(propertyList) {
    199             itemProvider.loadItem(forTypeIdentifier: propertyList, options: nil, completionHandler: { (item, error) -> Void in
    200                 guard let dictionary = item as? NSDictionary else { return }
    201                 if error != nil {
    202                     self.highlighter_state = .failed(error: "Error loading plist item: \(error?.localizedDescription ?? "Unknown")")
    203                     return
    204                 }
    205                 OperationQueue.main.addOperation {
    206                     if let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary,
    207                        let urlString = results["URL"] as? String,
    208                        let selection = results["selectedText"] as? String,
    209                        let url = URL(string: urlString) {
    210                         guard selection != "" else {
    211                             self.highlighter_state = .no_highlight_text
    212                             return
    213                         }
    214                         self.highlighter_state = .loaded(highlighted_text: selection, source_url: url)
    215                     }
    216                     else {
    217                         self.highlighter_state = .failed(error: "Cannot load results")
    218                     }
    219                 }
    220             })
    221         }
    222         else {
    223             self.highlighter_state = .failed(error: "No plist detected")
    224         }
    225     }
    226     
    227     func post(_ post: NostrPost) {
    228         self.highlighter_state = .posting
    229         guard let state else {
    230             self.highlighter_state = .failed(error: "Damus state not initialized")
    231             return
    232         }
    233         guard let full_keypair = state.keypair.to_full() else {
    234             self.highlighter_state = .not_logged_in
    235             return
    236         }
    237         guard let posted_event = post.to_event(keypair: full_keypair) else {
    238             self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event")
    239             return
    240         }
    241         state.postbox.send(posted_event, on_flush: .once({ flushed_event in
    242             if flushed_event.event.id == posted_event.id {
    243                 DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {  // Offset labor perception bias
    244                     self.highlighter_state = .posted(event: flushed_event.event)
    245                 })
    246             }
    247             else {
    248                 self.highlighter_state = .failed(error: "Flushed event is not the event we just tried to post.")
    249             }
    250         }))
    251     }
    252     
    253     func done() {
    254         self.extensionContext.completeRequest(returningItems: [], completionHandler: nil)
    255     }
    256     
    257     enum HighlighterState: Equatable {
    258         case loading
    259         case no_highlight_text
    260         case not_logged_in
    261         case loaded(highlighted_text: String, source_url: URL)
    262         case posting
    263         case posted(event: NostrEvent)
    264         case cancelled
    265         case failed(error: String)
    266     }
    267 }
    268 
    269 class ActionViewController: UIViewController {
    270     override func viewDidLoad() {
    271         super.viewDidLoad()
    272         self.view.tintColor = UIColor(DamusColors.purple)
    273         
    274         DispatchQueue.main.async {
    275             let contentView = UIHostingController(rootView: ShareExtensionView(extensionContext: self.extensionContext!))
    276             self.addChild(contentView)
    277             self.view.addSubview(contentView.view)
    278             
    279             // set up constraints
    280             contentView.view.translatesAutoresizingMaskIntoConstraints = false
    281             contentView.view.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
    282             contentView.view.bottomAnchor.constraint (equalTo: self.view.bottomAnchor).isActive = true
    283             contentView.view.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
    284             contentView.view.rightAnchor.constraint (equalTo: self.view.rightAnchor).isActive = true
    285         }
    286     }
    287 }